// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: junglekim
#pragma once

#include <cmath>
#include <limits>
#include <string>

#include "coin2/exchange/base/symbology/product.h"
#include "coin2/strategy/order_executor/layering_executor3.h"
#include "coin2/strategy/util/moving_ohlc.h"
#include "presto/quant/math/moving_average.h"

using coin::proto::OrderDuration;
using coin::proto::OrderEvent;
using coin::proto::OrderSide;
using coin::proto::OrderType;
using coin::proto::TradeSide;
using coin2::app::impl::Driver;
using coin2::exchange::base::order::IOrderSubsystem;
using coin2::exchange::base::order::OrderSpec;
using coin2::exchange::base::order::OrderUpdate;
using coin2::exchange::base::symbology::IProduct;
using coin2::exchange::base::symbology::IProductInfo;
using coin2::strategy::order_executor::LayeringExecutor3;
using coin2::strategy::util::MovingHigh;
using coin2::strategy::util::MovingLow;

namespace coin2::strategy::vmm {

inline double BilinearInterpolation(
    double q11,
    double q12,
    double q21,
    double q22,
    double x1,
    double x2,
    double y1,
    double y2,
    double x,
    double y) {
  double x2x1, y2y1, x2x, y2y, yy1, xx1;
  x2x1 = x2 - x1;
  y2y1 = y2 - y1;
  x2x = x2 - x;
  y2y = y2 - y;
  yy1 = y - y1;
  xx1 = x - x1;
  return 1.0 / (x2x1 * y2y1) *
         (q11 * x2x * y2y + q21 * xx1 * y2y + q12 * x2x * yy1 + q22 * xx1 * yy1);
}

inline std::vector<double> linspace(double start, double end, int num) {
  if (num < 2) return std::vector<double>(1, start);
  int partitions = num - 1;
  std::vector<double> pts;
  double length = (end - start) / partitions;
  pts.push_back(start);
  for (int i = 1; i < num - 1; i++) {
    pts.push_back(start + i * length);
  }
  pts.push_back(end);
  return pts;
}

inline std::vector<double> logspace(double start, double stop, int num, double base = 10) {
  if (num < 2) return std::vector<double>(1, std::pow(base, start));
  int partitions = num - 1;
  std::vector<double> pts;
  double length = (stop - start) / partitions;
  pts.push_back(std::pow(base, start));
  for (int i = 1; i < num - 1; i++) {
    pts.push_back(std::pow(base, start + i * length));
  }
  pts.push_back(std::pow(base, stop));
  return pts;
}

inline double normalCDF(double value) { return 0.5 * erfc(-value * M_SQRT1_2); }

inline int64_t Readable2Epoch(std::string readable) {
  struct tm time;
  int64_t result;
  strptime(readable.c_str(), "%Y-%m-%d %H:%M:%S", &time);
  result = (mktime(&time) - timezone) * 1'000'000'000LL;

  return result;
}

inline std::string GetSymbol(const IProduct& product) {
  return product.market_name() + ":" + product.exchange_name() + ":" + product.absolute_norm();
}

inline double DecayEdge(
    int64_t decay_elapsed_time,
    double decay_gradient,
    double decay_intercept,
    double decay_min,
    double default_edge) {
  if (decay_elapsed_time > decay_min * 60e9) {
    return default_edge;
  }
  return (decay_gradient * decay_elapsed_time) + decay_intercept;
}

inline const std::pair<std::optional<double>, std::optional<double>> InnermostOrderPrice(
    IOrderSubsystem* os) {
  auto& awo = os->gateway()->order_manager().GetAllWorkingOrders();
  std::pair<std::optional<double>, std::optional<double>> result = {std::nullopt, std::nullopt};
  for (auto& order : awo) {
    if (order->order_side() == coin::proto::OrderSide::BUY_ORDER) {
      result.first =
          !result.first.has_value()
              ? std::optional(order->order_price())
              : result.first > order->order_price() ? result.first : order->order_price();
    } else {
      result.second =
          !result.second.has_value()
              ? std::optional(order->order_price())
              : result.second < order->order_price() ? result.second : order->order_price();
    }
  }
  return result;
}

inline const std::pair<std::optional<double>, std::optional<double>> InnermostOrderQty(
    IOrderSubsystem* os) {
  auto& awo = os->gateway()->order_manager().GetAllWorkingOrders();
  std::pair<std::optional<double>, std::optional<double>> result = {std::nullopt, std::nullopt};
  std::pair<std::optional<double>, std::optional<double>> result_q = {std::nullopt, std::nullopt};
  for (auto& order : awo) {
    if (order->order_side() == coin::proto::OrderSide::BUY_ORDER) {
      result.first =
          !result.first.has_value()
              ? std::optional(order->order_price())
              : result.first > order->order_price() ? result.first : order->order_price();
      result_q.first =
          !result_q.first.has_value()
              ? std::optional(order->working_qty())
              : result.first > order->order_price() ? result_q.first : order->working_qty();
    } else {
      result.second =
          !result.second.has_value()
              ? std::optional(order->order_price())
              : result.second < order->order_price() ? result.second : order->order_price();
      result_q.second =
          !result_q.second.has_value()
              ? std::optional(order->working_qty())
              : result.second < order->order_price() ? result_q.second : order->working_qty();
    }
  }
  return result_q;
}

struct Centreback {
  Centreback(int side, double tp, double minpp, double maxpp, double value, int ln, int wh) {
    if (side == -1) {
      CHECK_GT(minpp, tp);
      CHECK_GT(maxpp, minpp);
      sell = true;
      sell_trigger_pct = tp;
      sell_min_price_pct = minpp;
      sell_max_price_pct = maxpp;
      sell_value = value;
      sell_layer_num = ln;
      sell_window_hour = wh;
      sell_moving_low.reset(new MovingLow<double>(sell_window_hour * 60e9 * 60));
    } else if (side == 1) {
      CHECK_GT(maxpp, tp);
      CHECK_GT(minpp, maxpp);
      buy = true;
      buy_trigger_pct = tp;
      buy_min_price_pct = minpp;
      buy_max_price_pct = maxpp;
      buy_value = value;
      buy_layer_num = ln;
      buy_window_hour = wh;
      buy_moving_high.reset(new MovingHigh<double>(buy_window_hour * 60e9 * 60));
    }
  }

  bool sell = false;
  bool sell_deployed = false;
  bool sell_deploying = true;
  double sell_trigger_pct = 0.0;
  double sell_trigger_price = 0.0;
  double sell_min_price_pct = 0.0;
  double sell_max_price_pct = 0.0;
  double sell_value = 0.0;
  double sell_qty = 0.0;
  int sell_layer_num = 0;
  int sell_window_hour = 0;
  int64_t sell_ts = 0;
  std::unique_ptr<MovingLow<double>> sell_moving_low;

  bool buy = false;
  bool buy_deployed = false;
  bool buy_deploying = true;
  double buy_trigger_pct = 0.0;
  double buy_trigger_price = 0.0;
  double buy_min_price_pct = 0.0;
  double buy_max_price_pct = 0.0;
  double buy_value = 0.0;
  double buy_qty = 0.0;
  int buy_layer_num = 0;
  int buy_window_hour = 0;
  int64_t buy_ts = 0;
  std::unique_ptr<MovingHigh<double>> buy_moving_high;
  Driver* driver_;
  std::unique_ptr<LayeringExecutor3> executor;

  void InitExecutor(
      const PassiveExecutorConfig& og_config,  // OrderGateway(x) OriginalGangster(o)
      int64_t timestamp,
      Driver* driver) {
    driver_ = driver;
    auto defender_config = new PassiveExecutorConfig();
    defender_config->MergeFrom(og_config);
    defender_config->set_filter_tag("CB");
    defender_config->set_max_posting_period(
        std::max(sell_window_hour, buy_window_hour) * 60 * 60e9);
    auto get_product_holder = [this](const IProduct& product, int64_t timestamp) {
      return driver_->product_holder_manager()->GetProductHolder(product, timestamp);
    };
    executor = std::make_unique<LayeringExecutor3>(
        *defender_config,
        driver_->order()->GetUniqueSystem(MarketExchangeApi::FromString(defender_config->mea())),
        timestamp,
        nullptr,
        get_product_holder);
  }

  void DeployCB(int side, int64_t timestamp) {
    auto pi = &(executor->product_info());
    std::string filter_tag = "CB";
    if (side == -1 && sell_deploying) {
      std::vector<double> prices = logspace(
          log10(sell_trigger_price * (1 + sell_min_price_pct / 100)),
          log10(sell_trigger_price * (1 + sell_max_price_pct / 100)),
          sell_layer_num);
      double price_sum;
      for (const auto& price : prices) {
        price_sum += pi->tick_price().RoundUp(price);
      }
      double lot_size = pi->tick_qty().Round(sell_value / price_sum);
      for (const auto& price : prices) {
        OrderSpec order_spec(
            executor->product(),
            OrderType::LIMIT_ORDER,
            OrderSide::SELL_ORDER,
            OrderDuration::GTC_ORDER,
            price,
            lot_size,
            timestamp,
            true,
            0,
            filter_tag);
        driver_->order()
            ->GetUniqueSystem(MarketExchangeApi::FromString(executor->config().mea()))
            ->gateway()
            ->SubmitOrder(order_spec);
        executor->UpdateState(timestamp);
      }
      sell_ts = timestamp;
      sell_deploying = false;
    } else if (side == 1 && buy_deploying) {
      std::vector<double> prices = logspace(
          log10(buy_trigger_price / (1 + buy_min_price_pct / 100)),
          log10(buy_trigger_price / (1 + buy_max_price_pct / 100)),
          buy_layer_num);
      double price_sum;
      for (const auto& price : prices) {
        price_sum += pi->tick_price().RoundDown(price);
      }
      double lot_size = pi->tick_qty().Round(buy_value / price_sum);
      reverse(prices.begin(), prices.end());
      for (const auto& price : prices) {
        OrderSpec order_spec(
            executor->product(),
            OrderType::LIMIT_ORDER,
            OrderSide::BUY_ORDER,
            OrderDuration::GTC_ORDER,
            price,
            lot_size,
            timestamp,
            true,
            0,
            filter_tag);
        driver_->order()
            ->GetUniqueSystem(MarketExchangeApi::FromString(executor->config().mea()))
            ->gateway()
            ->SubmitOrder(order_spec);
        executor->UpdateState(timestamp);
      }
      buy_ts = timestamp;
      buy_deploying = false;
    }
  }

  void Check2Deploy(int side, double midp, int64_t timestamp) {
    if (side == -1) {
      if ((sell_moving_low->Get() * (1 + sell_trigger_pct / 100) < midp && !sell_deployed) ||
          (sell_deployed && sell_deploying)) {
        if (!sell_deployed) {
          sell_deployed = true;
          sell_trigger_price = sell_moving_low->Get();
        }
        DeployCB(-1, timestamp);
      } else if (sell_deployed && timestamp - sell_ts > sell_window_hour * 60 * 60e9) {
        executor->ManageOrders(timestamp);
        executor->ForceCancelAllWorkingOrders();
        sell_deployed = false;
        sell_deploying = true;
        sell_qty = 0.0;
      }
    } else if (side == 1) {
      if ((buy_moving_high->Get() * (1 - buy_trigger_pct / 100) > midp && !buy_deployed) ||
          (buy_deployed && buy_deploying)) {
        if (!buy_deployed) {
          buy_deployed = true;
          buy_trigger_price = buy_moving_high->Get();
        }
        DeployCB(1, timestamp);
      } else if (buy_deployed && timestamp - buy_ts > buy_window_hour * 60 * 60e9) {
        executor->ManageOrders(timestamp);
        executor->ForceCancelAllWorkingOrders();
        buy_deployed = false;
        buy_deploying = true;
        buy_qty = 0.0;
      }
    }
  }

  void Update(double midp, int64_t timestamp) {
    if (sell) {
      sell_moving_low->Add(timestamp, midp);
      Check2Deploy(-1, midp, timestamp);
    }
    if (buy) {
      buy_moving_high->Add(timestamp, midp);
      Check2Deploy(1, midp, timestamp);
    }
  }
};

struct ApproachPhase {
  int buysell = 0;
  double target_pos_change = 0;
  int64_t timestamp0 = 0;
  int64_t timestamp1 = std::numeric_limits<int64_t>::max();
  double price0 = 0;
  double price1 = std::numeric_limits<double>::max();
  double period_min = 0;
  double min_price = 0;
  double max_price = 0;
  int phase_num = 0;

  bool CheckPhase(int64_t timestamp, double midp) {
    return (timestamp0 < timestamp) && (timestamp < timestamp1) && (price0 < midp) &&
           (midp < price1);
  }

  bool operator!=(const ApproachPhase& rhs) const { return phase_num != rhs.phase_num; }
};

struct Bbo {
  int64_t timestamp = 0;
  std::string symbol = "";
  double ask0p = 0;
  double ask0q = 0;
  double bid0p = 0;
  double bid0q = 0;
  double midp = 0;
  int64_t seqno = 0;

  bool is_ready() const { return seqno > 3; }

  void CopyAndIncSeqNo(const Bbo& bbo) {
    timestamp = bbo.timestamp;
    symbol = bbo.symbol;
    ask0p = bbo.ask0p;
    ask0q = bbo.ask0q;
    bid0p = bbo.bid0p;
    bid0q = bbo.bid0q;
    midp = bbo.midp;
    seqno++;
  }
};

struct Vpin {
  std::string symbol = "";
  int64_t cur_time = 0;
  int64_t decay_start_time = 0;
  double vpin = -1;
  double cur_size = 0;
  double cur_buy = 0;
  double cur_sell = 0;
  double decay_gradient = 0;
  double decay_intercept = 0;
  double cur_edge = 0;
  bool decaying = false;

  bool is_ready() const { return vpin >= 0; }

  void Update(
      double side,
      double qty,
      int64_t tr_time,
      double basket_size,
      double max_edge,
      double default_edge,
      double decay_min) {
    bool flag = false;
    cur_time = tr_time;
    cur_size = cur_size + qty;

    if (cur_size > basket_size) {
      if (cur_size / basket_size >= 2) {
        cur_size = fmod(cur_size, basket_size);
        vpin = 1;
        if (side == 1) {
          cur_buy = cur_size;
          cur_sell = 0;
        } else {
          cur_sell = cur_size;
          cur_buy = 0;
        }
      } else {
        cur_size = cur_size - basket_size;
        if (side == 1) {
          vpin = std::min(std::abs(basket_size - (2 * cur_sell)) / basket_size, 1.0);
          cur_buy = cur_size;
          cur_sell = 0;
        } else {
          vpin = std::min(std::abs(basket_size - (2 * cur_buy)) / basket_size, 1.0);
          cur_sell = cur_size;
          cur_buy = 0;
        }
      }

      if (vpin > 0.5) {
        if (!decaying || cur_edge < vpin * max_edge) {
          cur_edge = vpin * max_edge;
          decay_intercept = vpin * max_edge;
          decay_start_time = cur_time;
          decay_gradient = (default_edge - decay_intercept) / (decay_min * 60e9);
          decaying = true;
        } else {
          flag = true;
        }
      }
    } else if (side == 1) {
      cur_buy = cur_buy + qty;
      if (decaying) {
        flag = true;
      }
    } else {
      cur_sell = cur_sell + qty;
      if (decaying) {
        flag = true;
      }
    }

    if (flag) {
      int64_t decay_elapsed_time = cur_time - decay_start_time;
      cur_edge =
          DecayEdge(decay_elapsed_time, decay_gradient, decay_intercept, decay_min, default_edge);
    }

    if (cur_edge <= default_edge) {
      decaying = false;
      cur_edge = default_edge;
    }
    // LOG(INFO) << cur_time << " " << symbol << " vpin " << vpin << " cur_edge " << cur_edge << "
    // default " << default_edge << std::endl;
  }
};

enum TimerType { CONTROLLER, FEED };
}  // namespace coin2::strategy::vmm
